contents
1. Java에서 Null 안전(null safety)의 중요성
- NullPointerException(NPE) 은 Java에서 가장 흔한 런타임 오류 중 하나입니다.
객체 참조가null일 때 메서드 호출(obj.method())을 하면 NPE가 발생합니다. - Java의 모든 참조형 변수는 언제든 null이 될 수 있어, 체이닝 혹은 값 접근 시 항상 주의해야 합니다.
2. Java의 Optional이란?
Optional<T>은 값이 없을 수도 있음을 명시적으로 설명하는 상자(컨테이너) 객체입니다.- Java 8에 추가되었으며, null 체크의 실수를 줄이고 “값 없음”을 코드로 명확히 표현할 수 있게 합니다.
- 모든 상황에서 쓰라는 것이 아니라, “값이 없을 수도 있음”을 표현하는 반환 값에 주로 사용합니다.
기본 사용법
Optional<String> opt = Optional.ofNullable(maybeString);
if (opt.isPresent()) {
System.out.println(opt.get());
} else {
System.out.println("값이 없습니다!");
}
3. Optional 객체 생성법
Optional<String> empty = Optional.empty(); // 값 없음
Optional<String> maybe = Optional.of("Hello"); // null 아님
Optional<String> safe = Optional.ofNullable(possibleNullString); // null 혹은 값
4. Optional 주요 메서드 및 활용 패턴
| 메서드/패턴 | 용도/설명 |
|---|---|
isPresent()/isEmpty() |
값 존재 확인 (ifPresent() 사용 권장) |
ifPresent(Consumer) |
값이 있으면 실행 (ex: opt.ifPresent(val -> ...)) |
get() |
값 반환, 값 없으면 예외(NoSuchElementException) 발생, 피해야 함 |
orElse(T) |
값이 있으면 반환/없으면 기본값 |
orElseGet(Supplier) |
값이 있으면 반환/없으면 supplier 함수 실행 |
orElseThrow(ExceptionSupplier) |
값이 있으면 반환/없으면 예외 발생 |
map(Function) |
값 있을 때 변환 |
flatMap(Function<Optional>) |
중첩 Optional을 한 번에 풀 때 |
filter(Predicate) |
조건 만족하는 값만 유지 |
예시:
Optional<String> value = Optional.ofNullable(getUserName());
String safeName = value.orElse("anonymous");
String upper = value
.filter(s -> s.length() > 2)
.map(String::toUpperCase)
.orElse("DEFAULT");
5. 실전 Optional 활용법
a. 안전한 체인 접근
public Optional<Address> findAddress(User user) {
return Optional.ofNullable(user)
.map(User::getProfile)
.map(Profile::getAddress);
}
b. 값이 있을 때 동작
opt.ifPresent(address -> System.out.println(address.getStreet()));
c. 기본값 지정
String name = optionalName.orElse("Default");
String location = optionalAddress.map(Address::getCity).orElse("Seoul");
d. 값 없음 시 예외 던지기
User user = findUser(id).orElseThrow(() -> new NotFoundException("User not found!"));
e. Optional 컬렉션 처리
List<Optional<User>> maybeUsers = ...;
List<User> users = maybeUsers.stream()
.flatMap(Optional::stream)
.collect(Collectors.toList());
6. Java에서 Null 안전 처리 (Optional 사용 유무 비교)
전통적 null 처리
if (user != null && user.getProfile() != null) {
String address = user.getProfile().getAddress();
}
Optional이용 (더 선언적)
String address = Optional.ofNullable(user)
.map(User::getProfile)
.map(Profile::getAddress)
.orElse("No Address");
기타 null 안전 기법
- 확인 후 접근:
if (myObj != null) { ... } - Apache Commons 사용:
StringUtils.isNotEmpty(str) - null 대신 빈 컬렉션 반환
public List<User> getUsers() { return users != null ? users : Collections.emptyList(); } - 문자열 비교 시 안전하게:
"hello".equals(s)를 사용하면 s가 null이어도 NPE 방지
7. Optional 적절 사용 시점
- 권장: null이 나올 수 있는 메서드의 반환 타입(예: find 계열)
- 지양: 클래스 필드, 컬렉션의 요소, 메서드 파라미터
- get() 직접 사용 금지: 반드시 orElse/ifPresent/예외 처리와 함께 사용할 것
8. Optional 오용 및 반패턴
.get()을 아무 검사 없이 호출하면 안 됨 (NoSuchElementException 발생)- 필드에서 사용하면 직렬화/호환성에 문제
- Optional을 파라미터로 사용하는 메서드도 좋지 않음
9. Java의 Null 안전 vs Kotlin
- Java: 타입 수준에서 null 안전 제공 X, Optional로 반환값만 보강 가능
- Kotlin:
String?등으로 컴파일러가 null 체크 강제,?.안전 호출 연산자 등 지원
Java에서는 NPE 발생 가능성을 항상 염두에 두고 개발해야 합니다.
10. 전체 활용 예시
public Optional<User> findUserById(int id) {
return id > 0 ? Optional.of(new User("Alice")) : Optional.empty();
}
// 사용
User user = findUserById(id)
.filter(User::isActive)
.orElseThrow(() -> new IllegalArgumentException("No active user"));
11. Optional/Null 안전 주요 패턴 요약
| 패턴 / 메서드 | 용도/설명 |
|---|---|
Optional.of(value) |
null 불가한 값 래핑 (null이면 예외) |
Optional.ofNullable(value) |
null 가능 값 래핑 |
Optional.empty() |
값 없음 |
isPresent()/ifPresent() |
값 존재 확인/처리 |
orElse()/orElseGet() |
값 없으면 기본/함수형 기본값 반환 |
orElseThrow() |
값 없으면 예외 던짐 |
map()/flatMap() |
존재 시 변환, 체이닝 |
filter() |
조건 걸러내기 |
| 전통 null 체크 | if (obj != null) |
| StringUtils 등 유틸 | StringUtils.isNotEmpty(str) |
| 값 비교시 안전 equals | "val".equals(str) |
| null 대신 빈 컬렉션 반환 | Collections.emptyList() 사용 |
정리:
Java의 Optional은 모든 null의 대체재는 아니지만, 반환값(특히 "값이 없을 수 있음")에서 null 안전성 향상과 명시적 사용을 지원합니다. Optional과 방어적 코딩, 유틸리티 함수 활용, 혹은 설계 단계에서 null 아예 회피까지 함께 고려하면 코드 품질이 크게 향상됩니다.
1. 핵심 개념 요약 (Concept Summary)
| Java (Optional 및 기존 방식) | Kotlin (Null-Safety 내장) | |
|---|---|---|
| 설계 | Null 허용, 예외(-Optionals) 없으면 NPE | 타입 시스템에서 null 안전을 언어 차원에서 보장 |
| 사용법 | 직접 null 체크, Optional |
타입에 ? 붙여 nullable 표현 및 연산 |
| 컴파일 | NPE 문제는 런타임에야 발견 (Optional, 애너테이션, 수작업 체크 필요) | 컴파일러가 미사용 시점에서 NPE 가능성 강제 차단 |
| 문법 | Optional<T> 객체, map/filter/ifPresent/orElse 등 체이닝 |
T(non-null), T?(nullable), safe call(?.) 등 |
| 제공기능 | 값 래핑/해제, 체이닝, orElse, orElseThrow, map 등 | 안전 호출, Elvis(?:), not-null assertion(!!), let, filterNotNull 등 |
| 성능 | Optional은 객체/박싱 오버헤드, 불필요하게 오용될 수 있음 | nullable 타입은 별도 객체 생성 없음 (성능/메모리 부담 거의 없음) |
2. 구체적 비교
A. Java의 null & Optional
1) null 체크 & NPE
String name = user != null ? user.getName() : "Default";
여전히 간과하면 NPE 가능.
2) Optional 사용
Optional<String> name = Optional.ofNullable(user)
.map(User::getName);
String actual = name.orElse("Default");
- 체이닝, 기본값, 예외 처리 등 명시적으로 값의 부재를 표현 가능
- 하지만 Optional 사용도 강제화되지 않으며, 필드나 파라미터로는 권장되지 않음
- Optional 변수도 null 지정이 가능해(실수로) 완전한 null 안전을 보장할 수 없음
B. Kotlin의 null-safety
1) 타입 시스템에서 null 여부 명확화
var name: String = "hello" // null불가
var nickname: String? = null // null 가능
// name = null // 컴파일 오류!
2) 안전 호출 연산자 (Safe call: ?.)
val user: User? = ...
val city: String? = user?.address?.city
- user가 null이면 이후 체이닝도 null 반환, 절대 NPE 발생 X
3) Elvis 연산자 (?:)
val name: String? = user?.name ?: "Default"
- 왼쪽이 null이면 오른쪽 기본값 반환
4) let/also 등 고차 함수 활용
user?.let { println(it.name) }
5) 강제 해제 연산자 (!!)
val n = name!! // null이면 즉시 NPE, 사용 권장 X
6) filterNotNull
val nullableList: List<String?> = listOf("a", null, "b")
val onlyNotNull: List<String> = nullableList.filterNotNull()
7) 컴파일러 수준 null 보호
- nullable 타입은 미사용 시점(컴파일 단계)에서 오류
- 플랫폼 타입(자바 코드에서 넘어온 것)은 주의 필요
3. 예제 코드 비교
Java
public String safeCity(User user) {
return Optional.ofNullable(user)
.map(User::getProfile)
.map(Profile::getAddress)
.map(Address::getCity)
.orElse("Seoul");
}
Kotlin
fun safeCity(user: User?): String =
user?.profile?.address?.city ?: "Seoul"
- 훨씬 간결하며, 체이닝이 쉽고, 실수로 skip한 null 체크를 컴파일 시 잡아냄
4. 설계적 & 실무적 차이
- Kotlin: null 안전이 언어 타입 자체에 완전히 내장
- 실수로 null을 할당/리턴/대입하는 순간 컴파일러가 잡아냄
- NPE는 !!(강제해제), 외부 자바 연동 등 일부 예외적인 경우에만 발생
- Java: Optional이 도입되었지만, 여전히 수작업 null 체크 코드가 남고, 완전히 강제하지 못함
- Optional 변수에도 null 저장 가능, 필드/파라미터 등 사용 제한 있음
- 타입 시스템 자체로는 null 안전 불가 (애너테이션 등 혼용)
5. 요약 테이블
| 항목 | Java(Optional 포함) | Kotlin (nullable 타입 내장) |
|---|---|---|
| null 가능 타입 | 불명확(Optional로 일부 명시) | 타입 선언 자체로 명시 (T? or T) |
| 컴파일 단계 체크 | 일부(애너테이션/Optional 등) | 컴파일러가 강제 |
| null 안전 연산자 | 없음(Optional 체이닝/map/filter) | ?., ?:, !!, let, filterNotNull 등 |
| null 허용 필드 | 모든 참조형 변수 | ? 붙여야만 수용 가능 |
| 성능 | Optional 오버헤드 有 | 오버헤드 거의 無 (박싱 없음) |
6. 결론 및 실전 적용
- Kotlin은 언어 차원에서 null을 컴파일러가 자동 체크/제어하기 때문에, 실수로 인한 NPE를 대부분 원천 차단할 수 있습니다(안전 nullable 타입, 연산자 보조).
- Java는 Optional의 도입에도 불구, null 실수 가능성이 남아 있으며, 여러 스타일(null, Optional, Null 애너테이션 등)이 혼존.
- Kotlin 코드는 더 짧고 명확하며, null 안전에 대해 개발자가 쉽게 안심할 수 있습니다.
references